package com.dxy.server;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.dxy.server.utils.PatternUtil;
import com.dxy.server.utils.PropertiesUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.UUID;

/**
 * @Description 业务处理线程
 *
 * 粘包拆包基本思路：让客户端发数据的时候在每条完整的json字符串后加结束标识符（如@@），
 * 我这里把收到的数据用split按@@分隔，在代码中定义一个全局变量first，用正则表达式匹配分隔好的每条数据，
 * 由于粘包分包/拆包/半包所造成的数据分隔后只有data[0]和data[data.length-1]的数据是不完整的，
 * 这里就分两种情况了：
 *
 * 第一种是分隔后的数据只有一条，那么用正则匹配这条数据，如果能匹配则说明该条数据是我们想要的，
 * 如果不是则说明这条数据是不完整的，将它追加给first
 * （如果出现粘包分包/拆包/半包问题，则第一组的最后一条数据和第二组的第一条数据合起来才是一条完整的数据）；
 *
 * 第二种是分隔后的数据大于一条，那么就需要用正则来匹配第一条和最后一条数据了，如果第一条不匹配则加上上一组的最后一条数据first，
 * 如果最后一条不匹配的话则再把这不完整的数据赋给first，以此类推。
 *
 * 这里需要注意的是当客户端第一次连接的时候发送的第一组的第一条的数据前半部分应该是完整的，
 * 就怕特殊状态下这第一次第一条的数据就是少头部的数据从半路来的数据，
 * 这样的话这组的第一条不完整的数据加上上一组的最后一条不完整的数据就构不成一条完整的我想要的数据了，
 * 所以这里我就把这条数据舍弃
 *
 * @Author dangxianyue
 * @Date 2023/3/19 11:10
 */
public class MyServerThread extends Thread {


    // 字符编码
    private String charset;
    // 读取缓冲区大小
    private int byteSize;
    // 最长数据接收时间间隔，单位秒
    private int readTimeout;

    // socket客户端连接
    private Socket socket;

    // 输入输出流
    private InputStream in;
    private OutputStream out;

    // 输出流，此处不用OutputStream是为了方便按行输出
    private PrintWriter printWriter;

    // 粘包拆包定界符
    private String delimiter;

    public MyServerThread(Socket socket) {
        this.socket = socket;
        this.charset = PropertiesUtil.getProperty("socket.charset");
        this.byteSize = Integer.parseInt(PropertiesUtil.getProperty("socket.byte-size"));
        this.readTimeout = Integer.parseInt(PropertiesUtil.getProperty("socket.read-timeout"));
        this.delimiter = PropertiesUtil.getProperty("socket.delimiter");
    }

    private void init() throws IOException {
        this.in = socket.getInputStream();
        this.out = socket.getOutputStream();
        this.printWriter = new PrintWriter(new OutputStreamWriter(out, charset));
    }

    private void close() {

        // 关闭流，最好是手动关闭，不要等到垃圾回收
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (printWriter != null) {
            printWriter.close();
        }

        // 关闭网络连接
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 线程正常生命周期结束自动退出
    }

    @Override
    public void run() {
        try {
            init();

            // 接收数据的等待超时时长，单位为毫秒，接收数据等待超时后,会出现 SocketTimeoutException
            socket.setSoTimeout(readTimeout*1000);

            // 从输入流缓冲区中读取数据放入数组中
            byte[] bytes = new byte[byteSize];

            // 每次从缓冲区读取到的数据量
            int len;

            // 如果出现粘包分包/拆包/半包问题，则第一组的最后一条数据和第二组的第一条数据合起来才是一条完整的数据
            // first表示上一组的最后一条数据
            StringBuilder first = new StringBuilder();

            // 此处读取会阻塞
            while ((len = in.read(bytes)) > 0) {
                String line = new String(bytes, 0, len, charset);
                String[] arr = line.split(delimiter);
                // "" ===> [], 1
                // "@@" ===> [], 0
                // "xxx" ===> [xxx], 1
                // "xxx@@" ===> [xxx], 1
                // "@@xxx" ===> [, xxx], 2
                // "xxx@@yyy" ===> [xxx, yyy], 2
                // "xxx@@yyy@@zzz" ===> [xxx, yyy, zzz], 3
                if (arr.length == 0) {
                    continue;
                }

                if (arr.length == 1) {
                    String dataPack = arr[0];
                    if (PatternUtil.matches(dataPack)) {
                        // 是完整的数据包
                        doBusiness(dataPack);
                        continue;
                    } else {
                        // 追加到first
                        first.append(dataPack);
                        if (PatternUtil.matches(first.toString())) {
                            // 已经凑齐一个完整的包
                            doBusiness(first.toString());
                            first.setLength(0);
                        }
                    }
                }

                if (arr.length > 1) {
                    for (int i = 0; i < arr.length; i++) {
                        if (i == 0) {
                            // 分割出的第一个片段
                            first.append(arr[i]);
                            if (PatternUtil.matches(first.toString())) {
                                // 凑齐了完整的包
                                doBusiness(first.toString());
                                first.setLength(0);
                            }
                        } else if (i == arr.length - 1) {
                            // 分割出的最后一个片段
                            if (PatternUtil.matches(arr[i])) {
                                doBusiness(arr[i]);
                            } else {
                                // 清空first后把本组的最后一个片段作为一个first
                                first.setLength(0);
                                first.append(arr[i]);
                            }
                        } else {
                            // 分割出的中间片段直接就是完整的包
                            doBusiness(arr[i]);
                        }
                    }
                }
            }


        } catch (Exception e) {
            // 超时会报错SocketTimeoutException，但不会通知到客户端
            // 客户端只能用心跳机制来维护socket连接
            if (e instanceof SocketTimeoutException) {
                send("超时未接受到数据，服务器将关闭连接");
            }
            e.printStackTrace();
            close();
        }

    }

    private void doBusiness(String message) {
        System.out.println("接收到数据：" + message);

        JSONObject jsonObject = JSON.parseObject(message);

        // do business
        try {
            System.out.println("进行业务处理");
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 响应消息给客户端
        JSONObject result = new JSONObject();
        result.put("transactionId", UUID.randomUUID().toString());
        result.put("dataId", jsonObject.get("dataId"));// 关键代码，请求和响应都要包含唯一对应的requestId
        result.put("success", true);
        result.put("message", "接收成功");
        send(result.toJSONString());
    }

    /**
     * 向客户端发送消息
     * @param message
     */
    private void send(String message) {
        printWriter.println(message);//ln表示直接在后面追加换行符
        printWriter.flush();
    }

}
